From 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b Mon Sep 17 00:00:00 2001 From: Fuwn <50817549+Fuwn@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:09:50 +0000 Subject: Initial commit Created from https://vercel.com/new --- .../[websiteId]/(reports)/funnels/Funnel.tsx | 134 ++++++++++++++++++++ .../(reports)/funnels/FunnelAddButton.tsx | 28 ++++ .../(reports)/funnels/FunnelEditForm.tsx | 141 +++++++++++++++++++++ .../[websiteId]/(reports)/funnels/FunnelsPage.tsx | 36 ++++++ .../[websiteId]/(reports)/funnels/page.tsx | 12 ++ 5 files changed, 351 insertions(+) create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx (limited to 'src/app/(main)/websites/[websiteId]/(reports)/funnels') diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx new file mode 100644 index 0000000..e336a3d --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx @@ -0,0 +1,134 @@ +import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useResultQuery } from '@/components/hooks'; +import { File, User } from '@/components/icons'; +import { ReportEditButton } from '@/components/input/ReportEditButton'; +import { ChangeLabel } from '@/components/metrics/ChangeLabel'; +import { Lightning } from '@/components/svg'; +import { formatLongNumber } from '@/lib/format'; +import { FunnelEditForm } from './FunnelEditForm'; + +type FunnelResult = { + type: string; + value: string; + visitors: number; + previous: number; + dropped: number; + dropoff: number; + remaining: number; +}; + +export function Funnel({ id, name, type, parameters, websiteId }) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading } = useResultQuery(type, { + websiteId, + ...parameters, + }); + + return ( + + + + + + + {name} + + + + + + {({ close }) => { + return ( + + + + ); + }} + + + + {data?.map( + ( + { type, value, visitors, previous, dropped, dropoff, remaining }: FunnelResult, + index: number, + ) => { + const isPage = type === 'path'; + return ( + + + + + {index + 1} + + + {index > 0 && ( + + )} + + + + + {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)} + + {formatMessage(labels.conversionRate)} + + + + {type === 'path' ? : } + {value} + + + {index > 0 && ( + + {formatLongNumber(dropped)} + + )} + + + + + {`${formatLongNumber(visitors)} ${formatMessage(labels.visitors)}`} + + + + + + + + {Math.round(remaining * 100)}% + + + + + + ); + }, + )} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx new file mode 100644 index 0000000..29b5480 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx @@ -0,0 +1,28 @@ +import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { FunnelEditForm } from './FunnelEditForm'; + +export function FunnelAddButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + {({ close }) => } + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx new file mode 100644 index 0000000..5d950ea --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx @@ -0,0 +1,141 @@ +import { + Button, + Column, + Form, + FormButtons, + FormField, + FormFieldArray, + FormSubmitButton, + Grid, + Icon, + Loading, + Row, + Text, + TextField, +} from '@umami/react-zen'; +import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks'; +import { Plus, X } from '@/components/icons'; +import { ActionSelect } from '@/components/input/ActionSelect'; +import { LookupField } from '@/components/input/LookupField'; + +const FUNNEL_STEPS_MAX = 8; + +export function FunnelEditForm({ + id, + websiteId, + onSave, + onClose, +}: { + id?: string; + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { data } = useReportQuery(id); + const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); + + const handleSubmit = async ({ name, ...parameters }) => { + await mutateAsync( + { ...data, id, name, type: 'funnel', websiteId, parameters }, + { + onSuccess: async () => { + touch('reports:funnel'); + touch(`report:${id}`); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (id && !data) { + return ; + } + + const defaultValues = { + name: data?.name || '', + window: data?.parameters?.window || 60, + steps: data?.parameters?.steps || [{ type: 'path', value: '' }], + }; + + return ( +
+ + + + + + + value.length > 1 || 'At least two steps are required', + }} + > + {({ fields, append, remove }) => { + return ( + + {fields.map(({ id }: { id: string }, index: number) => { + return ( + + + + + + + + + {({ field, context }) => { + const type = context.watch(`steps.${index}.type`); + return ; + }} + + + + + ); + })} + + + + + ); + }} + + + + {formatMessage(labels.save)} + +
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx new file mode 100644 index 0000000..57bce52 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx @@ -0,0 +1,36 @@ +'use client'; +import { Column, Grid } from '@umami/react-zen'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Panel } from '@/components/common/Panel'; +import { SectionHeader } from '@/components/common/SectionHeader'; +import { useDateRange, useReportsQuery } from '@/components/hooks'; +import { Funnel } from './Funnel'; +import { FunnelAddButton } from './FunnelAddButton'; + +export function FunnelsPage({ websiteId }: { websiteId: string }) { + const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' }); + const { + dateRange: { startDate, endDate }, + } = useDateRange(); + + return ( + + + + + + + {data && ( + + {data.data?.map((report: any) => ( + + + + ))} + + )} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx new file mode 100644 index 0000000..2fdcf3b --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { FunnelsPage } from './FunnelsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Funnels', +}; -- cgit v1.2.3